// FTPClient.cs
//
//  Copyright (C) 2008-2009 Christian Eide
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//

using System;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.IO;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Security.Cryptography.X509Certificates;
using Sarnata.Net.BareFtp.Preferences;

namespace Sarnata.Net.BareFtp.Protocol.Ftp
{	
	public class FTPClient
	{
        internal const string EOL = "\r\n";
		private static int DATA_PORT_RANGE_FROM = 1500;
		private static int DATA_PORT_RANGE_TO = 65000;
		private static int BLOCK_SIZE = 512;
		private List<string> features;
		private Config conf;
		private bool read;
		private bool abort = false;
		
		// default encoding
		//private System.Text.Encoding readerenc = System.Text.Encoding.ASCII;

		TcpClient tcpClient;
		StreamReader reader = null;
		StreamWriter writer = null;
		string charset = string.Empty;
		FTPMode ftpMode;
		bool SSL;
		string system;
		string prot_level = string.Empty;
		
		public string RemoteSystem
		{
			set { system = value; }
		}

		public List<string> Features
		{
			get { return features; }
		}
		
		public string RemoteCharset
		{
			set { charset = value; }
		}
		
		public FTPClient(FTPMode mode, bool SSL, Config conf)
		{
			this.conf = conf;
			tcpClient = new TcpClient();
			tcpClient.SendTimeout = conf.NetworkTimeout;
			tcpClient.ReceiveTimeout = conf.NetworkTimeout;
			ftpMode = mode;
			this.SSL = SSL;
		}
		
		public void Connect(string remoteHost, int remotePort, string user, string password)
		{
			
			tcpClient.Connect(Sarnata.Net.BareFtp.Protocol.HostNameResolver.GetAddress(remoteHost), remotePort);
			Stream stream = tcpClient.GetStream();
			
            writer = new StreamWriter(stream, System.Text.Encoding.ASCII);
			reader = new StreamReader(stream, System.Text.Encoding.ASCII);
			
			CheckReply(GetReply(), 220);
			
			try
			{
				reader = new StreamReader(stream, GetEncoding());
				writer = new StreamWriter(stream, System.Text.Encoding.ASCII);
			}
			catch(Exception ex)
			{
				this.Close();
				throw ex;
			}
			
			// Check if we need to use SSL
			if(SSL)
			{
				string plevel;
				if(!string.IsNullOrEmpty(ProtLevel))
					plevel = ProtLevel;
				else
					plevel = conf.FTPSDataChannelProtectionLevel;
				   
				CheckReply(SendCommand("AUTH TLS"), 234);
				
				SslStream sslstream = new SslStream(tcpClient.GetStream(), false, CertificateValidation);
				
				
				try
				{
					//sslstream.AuthenticateAsClient(remoteHost, 
					sslstream.AuthenticateAsClient(remoteHost);
				}
				catch(Exception exc)
				{
					
					reader.Close();
					reader.Dispose();
					reader = null;
					writer.Close();
					writer.Dispose();
					writer = null;
					tcpClient.Close();
					sslstream.Close();
					sslstream.Dispose();
					sslstream = null;
					
					throw new Exception("Certificate Error: " + exc.Message);
				}
		
				writer = new StreamWriter(sslstream, GetEncoding());
				reader = new StreamReader(sslstream, GetEncoding());
				
				CheckReply(SendCommand("PBSZ 0"), 200);
				CheckReply(SendCommand(string.Format("PROT {0}", plevel)), 200);
			}
			
			try
			{
				CheckReply(SendCommand("USER " + user), 331);
				CheckReply(SendCommand("PASS " + password), 230);
			}
			catch(Exception e)
			{
				
				stream.Close();
				stream.Dispose();
				stream = null;
				writer = null;
				reader = null;
				tcpClient.Close();
				tcpClient = null;
				throw e;
			}

			GetSiteFeatures();
			SetTransferType(Ftp.FTPFileTransferType.Binary);
		}
		
		public bool CertificateValidation (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
		    return true;
            if(sslPolicyErrors == SslPolicyErrors.None)
			{
				return true;
			}
            else
			{
				// TODO: Check out how to inspect the chain better...
				if(conf.FTPSVerifyServerCertificate)
				{
					OnLogTextEmitted(new LogTextEmittedArgs(-2, String.Format("Server certificate error: {0}", sslPolicyErrors)));
					OnLogTextEmitted(new LogTextEmittedArgs(-2, "Try disabling Server Certificate Validation"));
					return false;
				}					
				else
					return true;
			}
        }
		
		public void Close()
		{
			if(tcpClient.Connected)
				CheckReply(SendCommand("QUIT"), 221);
			if(tcpClient.Connected)
				tcpClient.Close();
		}
		
		public bool Connected
		{
			get {
				if(tcpClient != null)
					return tcpClient.Connected;
				else
					return false;
			}
		}

		public string ProtLevel {
			get {
				return prot_level;
			}
			set {
				prot_level = value;
			}
		}
		
		public string GetCurrentDirectory()
		{
			List<FTPReply> replies = CheckReply(SendCommand("PWD"), 257);
			return replies[0].Message;
		}
		
		public List<FTPReply> SendCommand(string command)
		{
				
			Byte[] cmdBytes = GetEncoding().GetBytes((command+EOL).ToCharArray());
			OnLogTextEmitted(new LogTextEmittedArgs(-1, command));
			writer.BaseStream.Write(cmdBytes, 0, cmdBytes.Length);
			writer.Flush();
			return GetReply();
			
		}
		
		public void ChangeDir(string dir)
		{
			CheckReply(SendCommand("CWD " + dir), 250);
		}
		
		public bool IsDir(string cwd, string dir)
		{
			bool isdir = false;
			foreach(FTPReply reply in SendCommand("CWD " + dir))
			{
				if(reply.ReplyCode == 250)
					isdir = true;
			}
			SendCommand("CWD " + cwd);
			return isdir;
		}
		
		public void MakeDir(string directoryName)
		{
			CheckReply(SendCommand("MKD " + directoryName), 257);
		}
		
		public List<string> XDir()
		{
			List<string> lines = new List<string>();
			
			SetTransferType(FTPFileTransferType.ASCII);
			
			if(ftpMode == FTPMode.Passive)
			{
				if(features.Contains("PRET") || features.Contains("pret"))
					CheckReply(SendCommand("PRET LIST"), true, 200);
			}
			
			DataSocket dsocket = CreateDataSocket();
			
			// Send the list command. Server should reply with 150
			string list_command = "LIST -La";
			//if(features.Contains("MLSD"))
			//	list_command = "MLSD";
			
			if(system.IndexOf("VMS") >= 0 || system.Contains("MultiNet Unix Emulation"))
				list_command = "LIST";
			
			List<FTPReply> replies = SendCommand(list_command);
			
			CheckReply(replies, 150, 125);
			
			lines = ReadLines(dsocket.GetStream());
			
			// Uncomment the following to dump directory listing in console
			//foreach(string l in lines)
			//	Console.WriteLine(l);
			
			dsocket.Close();			
			// Read reply from control socket. Should be 226
			CheckReply(GetReply(), 226);
			
			return lines;
		}
		
		public List<string> Dir()
		{
			List<string> lines = new List<string>();
			
			SetTransferType(FTPFileTransferType.ASCII);
			if(ftpMode == FTPMode.Passive)
			{
				if(features.Contains("PRET") || features.Contains("pret"))
					CheckReply(SendCommand("PRET NLST"), true, 200);
			}
			
			DataSocket dsocket = CreateDataSocket();
			CheckReply(SendCommand("NLST"), 150, 125, 550);
			lines = ReadLines(dsocket.GetStream());
			dsocket.Close();
			// Read reply from control socket. Should be 226
			CheckReply(GetReply(), 226);
		
			return lines;
		}
		
		public void RetrieveFile(XferFile file, FileAction action, System.IO.Stream fstream)
		{
			if(ftpMode == FTPMode.Passive)
			{
				if(features.Contains("PRET") || features.Contains("pret"))
				{
						CheckReply(SendCommand("PRET RETR " + file.Path.FileNameRemoteAbs), true, 200);
				}	
			}
			DataSocket dsocket = CreateDataSocket();
			
			SetTransferType(FTPFileTransferType.Binary);
			
			if(action == FileAction.Resume)
			{
				file.TransferedBytes = fstream.Length;
				CheckReply(SendCommand("REST " + fstream.Length.ToString()), 350);
			}
							
			CheckReply(SendCommand("RETR " + file.Path.FileNameRemoteAbs), 125, 150);
			
			file.Status = Sarnata.Net.BareFtp.Protocol.DownloadStatus.Downloading;
			
			Stream networkStream = dsocket.GetStream();
			Byte[] buffer = new Byte[BLOCK_SIZE];
			int bytes = 0;

			read = true;
			
			while(read)
			{
				bytes = (int)networkStream.Read(buffer, 0, buffer.Length);
				fstream.Write(buffer, 0, bytes);
				file.TransferedBytes += (long)bytes;

				if(bytes == 0)
					read = false;
			}
			
			if(abort)
			{
				networkStream.Close();
				dsocket.Close();
				file.Status = Sarnata.Net.BareFtp.Protocol.DownloadStatus.Aborted;
				abort = false;
				throw new ReconnectException();
			}
			else
			{
                file.Status = Sarnata.Net.BareFtp.Protocol.DownloadStatus.Finished;	
				networkStream.Close();
				dsocket.Close();
				CheckReply(GetReply(), 226);
			}
		}
		
		public void StoreFile(XferFile file, FileAction action, System.IO.Stream fstream)
		{
			
			//SetTransferType(FTPFileTransferType.Binary);
			
			if(ftpMode == FTPMode.Passive)
			{
				if(features.Contains("PRET") || features.Contains("pret"))
				{
					if(action == FileAction.Resume || action == FileAction.Append)
						CheckReply(SendCommand("PRET APPE " + file.Path.FileNameRemoteAbs), true, 200);
					else
						CheckReply(SendCommand("PRET STOR " + file.Path.FileNameRemoteAbs), true, 200);
				}
			}

			DataSocket dsocket = CreateDataSocket();
			long fslength = fstream.Length;
			if(action == FileAction.Resume)
			{
				CheckReply(SendCommand("APPE " + file.Path.FileNameRemoteAbs), 125, 150);
				fstream.Position = file.Marker;
				fslength = fslength - file.Marker;
				file.TransferedBytes = file.Marker;
			}
            else if (action == FileAction.Append)
                CheckReply(SendCommand("APPE " + file.Path.FileNameRemoteAbs), 125, 150);
            else
                CheckReply(SendCommand("STOR " + file.Path.FileNameRemoteAbs), 125, 150);

		    Stream networkStream = dsocket.GetStream();

			Byte[] buffer = new Byte[BLOCK_SIZE];
			int bytes = 0;

            file.Status = Sarnata.Net.BareFtp.Protocol.DownloadStatus.Uploading;
		
			while(file.TransferedBytes < fslength && file.Status != DownloadStatus.Aborted && !abort)
			{
				bytes = (int)fstream.Read(buffer, 0, BLOCK_SIZE);
				networkStream.Write(buffer, 0, bytes);
				file.TransferedBytes += (long)bytes;
			}
			
			networkStream.Close();
			dsocket.Close();
			
			if(abort)
			{
				file.Status = DownloadStatus.Aborted;
				abort = false;
				try
				{
					GetReply();
				}
				catch(NullReferenceException)
				{
					throw new ReconnectException();	
				}
			}
			else
			{
				file.Status = DownloadStatus.Finished;
				CheckReply(GetReply(), 226);
			}
		}
		
		public List<FTPReply> GetReply()
		{
			try
			{
				List<FTPReply> replies = new List<FTPReply>();
				
				// read first line
				//if(reader == null)
				//	return replies;
				
				string line = reader.ReadLine();
				
				while (line != null && line.Length == 0)
				{					
					System.Threading.Thread.Sleep(100);
					line = reader.ReadLine();
				}
				if (line == null)
					throw new SystemException("Unexpected null reply received");
            
				if (line.Length < 3)
					throw new SystemException("Short reply received");
            
				string replyCode = line.Substring(0, 3);
			
				string reply = "";
			
				if (line.Length > 4)
					reply = line.Substring(4);
			
				// check for multiline response and build up
				// the reply
				if (line[3] == '-')
				{
					bool complete = false;
					//reply += Environment.NewLine;
					while (!complete && line != null)
					{
						line = reader.ReadLine();
						if (line == null)
							throw new SystemException("Unexpected null reply received");
					
						if (line.Length == 0)
							continue;
                    
						if(line.Length > 3)
						{
							if (line.Substring(0,3) == replyCode && line[3] == ' ')
							{
								complete = true;
								reply += "\r\n" + line.Substring(4);
								//replies.Add(new FTPReply(Convert.ToInt32(tmpreplyCode), tmpreply));
							}
							else
								reply += "\r\n" + line;
						}
						else
							reply += "\r\n" + line;// + Environment.NewLine;
					}
				}
				
				replies.Add(new FTPReply(Convert.ToInt32(replyCode), reply));
				return replies;
			
			}
			catch(Exception ex)
			{
				throw ex;
			}
        }
		
		public void Delete(string filename)
		{
			CheckReply(SendCommand("DELE " + filename), 250);	
		}
		
		public void Abort()
		{
			read = false;
			abort = true;
		}
		
		public void RemoveDir(string dirname)
		{
			CheckReply(SendCommand("RMD " + dirname), 250);
		}

		public void Chmod(string mode, string filename)
		{
			Chmod(mode, filename, false);
		}
		
		public void Chmod(string mode, string filename, bool silent)
		{
			CheckReply(SendCommand(String.Format("SITE CHMOD {0} {1}", mode, filename)), silent, 200);
		}
		
		public void RenameFile(string fromRemoteFileName, string toRemoteFileName)
		{
		
			CheckReply(SendCommand("RNFR " + fromRemoteFileName), 350);
			CheckReply(SendCommand("RNTO " + toRemoteFileName), 250);
		}
		
		internal List<string> ReadLines(Stream stream)
		{
			List<string> lines = new List<string>();
			
			using(StreamReader input = new StreamReader(stream, GetEncoding()))
			{
				
				string line = null;
				while ((line = ReadLine(input)) != null)
					lines.Add(line);
            
				input.Close();
			}
			return lines;
		}
		
		internal virtual string ReadLine(TextReader input)
        {
			return input.ReadLine();
        }
		
		internal DataSocket CreateDataSocket()
        {            
			
			string hostIP = string.Empty;
			int port = 0;

            if (ftpMode == FTPMode.Passive)
            {
			    List<FTPReply> replies = CheckReply(SendCommand("PASV"), 227);
				
				FTPReply replyObj = null;
				foreach(FTPReply repObj in replies)
				{
					if(repObj.ReplyCode == 227)
						replyObj = repObj;
				}
				// If we have null here then throw exeption
				if(replyObj == null)
					throw new Exception("Did not receive proper response to PASV command");
				
	            string reply = replyObj.Message;
	            
	            Regex regEx = new Regex(@"(?<a0>\d{1,3}),(?<a1>\d{1,3}),(?<a2>\d{1,3}),(?<a3>\d{1,3}),(?<p0>\d{1,3}),(?<p1>\d{1,3})");
	            Match m1 = regEx.Match(reply);

	            string ipAddress = m1.Groups["a0"].Value + "." + m1.Groups["a1"].Value + "." + m1.Groups["a2"].Value + "." + m1.Groups["a3"].Value;
	            
	            int[] portParts = new int[2];
	            portParts[0] = Int32.Parse(m1.Groups["p0"].Value);
	            portParts[1] = Int32.Parse(m1.Groups["p1"].Value);
	            port = (portParts[0] << 8) + portParts[1];
	                     
				hostIP = ipAddress;

            }
            else if(ftpMode == FTPMode.Active)
            {
                Random rnd = new Random((int)DateTime.Now.Ticks);
                port = DATA_PORT_RANGE_FROM + rnd.Next(DATA_PORT_RANGE_TO - DATA_PORT_RANGE_FROM);

                int portHigh = port >> 8;
                int portLow = port & 255;

                CheckReply(SendCommand("PORT " + GetLocalAddressList()[0].ToString().Replace(".", ",")
                                         + "," + portHigh.ToString() + "," + portLow), 200);

            }
			
			string plevel;
			if(!string.IsNullOrEmpty(ProtLevel))
				plevel = ProtLevel;
			else
				plevel = conf.FTPSDataChannelProtectionLevel;
			
			return new DataSocket(hostIP, port, ftpMode, SSL, plevel, conf);
        }
		
		internal void SetTransferType(FTPFileTransferType type)
		{
			switch (type)
			{
				case FTPFileTransferType.ASCII:
					SetMode("TYPE A");
					break;
				case FTPFileTransferType.Binary:
					SetMode("TYPE I");
					break;
				default:
					throw new Exception("Invalid File Transfer Type");
			}
		}
			   
		internal void SetMode(string mode)
		{
			CheckReply(SendCommand(mode), 200);
		}
		
		private IPAddress[] GetLocalAddressList()
		{
			return Dns.GetHostEntry(Dns.GetHostName()).AddressList;
		}
		
		private void GetSiteFeatures()
		{
			features = new List<string>();
			try
			{
			
				foreach(FTPReply reply in CheckReply(SendCommand("FEAT"), true, 211))
				{
					if(reply.ReplyCode == 211)
					{
						using(StringReader sr = new StringReader(reply.Message))
						{
							string line = sr.ReadLine();
							while(line != null)
							{
								if(line.IndexOf("Features") < 0 && line.IndexOf("End") < 0)
									features.Add(line.Trim());
								line = sr.ReadLine();
							}
						}
					}
					
				}
			}
			catch(Exception)
			{
				features = new List<string>();
			}
		}
		
		public List<FTPReply> CheckReply(List<FTPReply> reply, params int[] codes)
		{
			return CheckReply(reply, false, codes);
		}
		
		public List<FTPReply> CheckReply(List<FTPReply> reply, bool silent, params int[] codes)
		{
			if(reply == null)
				return null;
			
			List<int> codelist = new List<int>(codes);
			
			if(silent)
			{
				foreach(FTPReply r in reply)
					r.Silent = silent;
			}
			
			OnLogTextEmitted(new LogTextEmittedArgs(reply));
			
			string msgstr = string.Empty;
			foreach(FTPReply r in reply)
			{
				msgstr += r.Message + System.Environment.NewLine;
				if(codelist.Contains(r.ReplyCode))
					return reply;
			}
			
			if(!silent)
				throw new FtpException(msgstr);
			else
				return reply;
		}
		
		private System.Text.Encoding GetEncoding()
		{
			if(!string.IsNullOrEmpty(charset))
				return System.Text.Encoding.GetEncoding(charset);
			if(!string.IsNullOrEmpty(conf.General_RemoteCharset))
				return System.Text.Encoding.GetEncoding(conf.General_RemoteCharset);
			if(features != null && features.Contains("UTF8"))
				return System.Text.Encoding.UTF8;
			
			return System.Text.Encoding.Default;
		}
		
		public event EventHandler LogTextEmitted;
		public virtual void OnLogTextEmitted(LogTextEmittedArgs e)
		{
			foreach (var r in e.Reply)
                Console.WriteLine(r.Message);
            //LogTextEmitted(this, e);
		}
		
		
	}
}
